add Agent Logs; add logging to WebsiteAgent; refactor flash notices and add event notices

Andrew Cantino 10 years ago
parent
commit
00727fbd4d

+ 3 - 0
.env.example

@@ -40,3 +40,6 @@ EMAIL_FROM_ADDRESS=from_address@gmail.com
40 40
 # This invitation code will be required for users to signup with your Huginn installation.
41 41
 # You can see its use in user.rb.
42 42
 INVITATION_CODE=try-huginn
43
+
44
+# Number of lines of log messages to keep per Agent
45
+AGENT_LOG_LENGTH=100

+ 34 - 7
app/assets/javascripts/application.js.coffee.erb

@@ -8,10 +8,6 @@
8 8
 #= require ./worker-checker
9 9
 #= require_self
10 10
 
11
-# Place all the behaviors and hooks related to the matching controller here.
12
-# All this logic will automatically be available in application.js.
13
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
14
-
15 11
 setupJsonEditor = ->
16 12
   JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>'
17 13
   JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>'
@@ -48,12 +44,43 @@ showEventDescriptions = ->
48 44
     $(".event-descriptions").html("").hide()
49 45
 
50 46
 $(document).ready ->
47
+  # JSON Editor
51 48
   setupJsonEditor()
52
-  $(".select2").select2(width: 'resolve')
53 49
 
54
-  if $(".top-flash").length
55
-    setTimeout((-> $(".top-flash").slideUp(-> $(".top-flash").remove())), 5000)
50
+  # Select2 Selects
51
+  $(".select2").select2(width: 'resolve')
56 52
 
53
+  # Flash
54
+  if $(".flash").length
55
+    setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000)
56
+
57
+  # Agent Show
58
+  fetchLogs = (e) ->
59
+    agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
60
+    e.preventDefault()
61
+    $("#logs .spinner").show()
62
+    $("#logs .refresh, #logs .clear").hide()
63
+    $.get "/agents/#{agentId}/logs", (html) =>
64
+      $("#logs .logs").html html
65
+      $("#logs .spinner").stop(true, true).fadeOut ->
66
+        $("#logs .refresh, #logs .clear").show()
67
+
68
+  clearLogs = (e) ->
69
+    if confirm("Are you sure you want to clear all logs for this Agent?")
70
+      agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
71
+      e.preventDefault()
72
+      $("#logs .spinner").show()
73
+      $("#logs .refresh, #logs .clear").hide()
74
+      $.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) =>
75
+        $("#logs .logs").html html
76
+        $("#logs .spinner").stop(true, true).fadeOut ->
77
+          $("#logs .refresh, #logs .clear").show()
78
+
79
+  $(".agent-show #show-tabs a[href='#logs']").on "click", fetchLogs
80
+  $("#logs .refresh").on "click", fetchLogs
81
+  $("#logs .clear").on "click", clearLogs
82
+
83
+  # Editing Agents
57 84
   $("#agent_source_ids").on "change", showEventDescriptions
58 85
 
59 86
   $("#agent_type").on "change", ->

+ 19 - 0
app/assets/javascripts/worker-checker.js.coffee

@@ -1,7 +1,11 @@
1 1
 $ ->
2
+  firstEventCount = null
3
+
2 4
   if $("#job-indicator").length
3 5
     check = ->
4 6
       $.getJSON "/worker_status", (json) ->
7
+        firstEventCount = json.event_count unless firstEventCount?
8
+
5 9
         if json.pending? && json.pending > 0
6 10
           tooltipOptions = {
7 11
             title: "#{json.pending} pending, #{json.awaiting_retry} awaiting retry, and #{json.recent_failures} recent failures"
@@ -12,5 +16,20 @@ $ ->
12 16
           $("#job-indicator").tooltip('destroy').tooltip(tooltipOptions).fadeIn().find(".number").text(json.pending)
13 17
         else
14 18
           $("#job-indicator:visible").tooltip('destroy').fadeOut()
19
+
20
+        if firstEventCount? && json.event_count > firstEventCount
21
+          $("#event-indicator").tooltip('destroy').
22
+                                tooltip(title: "Click to reload", delay: 0, placement: "bottom", trigger: "hover").
23
+                                fadeIn().
24
+                                find(".number").
25
+                                text(json.event_count - firstEventCount)
26
+        else
27
+          $("#event-indicator").tooltip('destroy').fadeOut()
28
+
15 29
         window.workerCheckTimeout = setTimeout check, 2000
30
+
16 31
     check()
32
+
33
+  $("#event-indicator a").on "click", (e) ->
34
+    e.preventDefault()
35
+    window.location.reload()

+ 26 - 1
app/assets/stylesheets/application.css.scss.erb

@@ -51,7 +51,7 @@ table.events {
51 51
   margin-left: 0 !important;
52 52
 }
53 53
 
54
-#job-indicator {
54
+#job-indicator, #event-indicator {
55 55
   display: none;
56 56
 }
57 57
 
@@ -85,3 +85,28 @@ img.spinner {
85 85
 .show-view {
86 86
   overflow: hidden;
87 87
 }
88
+
89
+// Flash
90
+
91
+.flash {
92
+  position: fixed;
93
+  width: 500px;
94
+  z-index: 99999;
95
+  top: 1px;
96
+  margin-left: 250px;
97
+
98
+  .alert {
99
+  }
100
+}
101
+
102
+// Logs
103
+
104
+#logs .action-icon {
105
+  height: 16px;
106
+  display: inline-block;
107
+  vertical-align: inherit;
108
+
109
+  &.refresh {
110
+    margin: 0 10px;
111
+  }
112
+}

+ 7 - 2
app/controllers/agents_controller.rb

@@ -9,8 +9,13 @@ class AgentsController < ApplicationController
9 9
   end
10 10
 
11 11
   def run
12
-    Agent.async_check(current_user.agents.find(params[:id]).id)
13
-    redirect_to agents_path, notice: "Agent run queued"
12
+    agent = current_user.agents.find(params[:id])
13
+    Agent.async_check(agent.id)
14
+    if params[:return] == "show"
15
+      redirect_to agent_path(agent), notice: "Agent run queued"
16
+    else
17
+      redirect_to agents_path, notice: "Agent run queued"
18
+    end
14 19
   end
15 20
 
16 21
   def type_details

+ 19 - 0
app/controllers/logs_controller.rb

@@ -0,0 +1,19 @@
1
+class LogsController < ApplicationController
2
+  before_filter :load_agent
3
+
4
+  def index
5
+    @logs = @agent.logs.all
6
+    render :action => :index, :layout => false
7
+  end
8
+
9
+  def clear
10
+    @agent.logs.delete_all
11
+    index
12
+  end
13
+
14
+  protected
15
+
16
+  def load_agent
17
+    @agent = current_user.agents.find(params[:agent_id])
18
+  end
19
+end

+ 1 - 2
app/controllers/worker_status_controller.rb

@@ -1,12 +1,11 @@
1 1
 class WorkerStatusController < ApplicationController
2
-  skip_before_filter :authenticate_user!
3
-
4 2
   def show
5 3
     start = Time.now.to_f
6 4
     render :json => {
7 5
         :pending => Delayed::Job.where("run_at <= ? AND locked_at IS NULL AND attempts = 0", Time.now).count,
8 6
         :awaiting_retry => Delayed::Job.where("failed_at IS NULL AND attempts > 0").count,
9 7
         :recent_failures => Delayed::Job.where("failed_at IS NOT NULL AND failed_at > ?", 5.days.ago).count,
8
+        :event_count => current_user.events.count,
10 9
         :compute_time => Time.now.to_f - start
11 10
     }
12 11
   end

+ 2 - 0
app/helpers/logs_helper.rb

@@ -0,0 +1,2 @@
1
+module LogsHelper
2
+end

+ 5 - 0
app/models/agent.rb

@@ -30,6 +30,7 @@ class Agent < ActiveRecord::Base
30 30
 
31 31
   belongs_to :user, :inverse_of => :agents
32 32
   has_many :events, :dependent => :delete_all, :inverse_of => :agent, :order => "events.id desc"
33
+  has_many :logs, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc"
33 34
   has_many :received_events, :through => :sources, :class_name => "Event", :source => :events, :order => "events.id desc"
34 35
   has_many :links_as_source, :dependent => :delete_all, :foreign_key => "source_id", :class_name => "Link", :inverse_of => :source
35 36
   has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver
@@ -139,6 +140,10 @@ class Agent < ActiveRecord::Base
139 140
     end
140 141
   end
141 142
 
143
+  def log(message, options = {})
144
+    AgentLog.log_for_agent(self, message, options)
145
+  end
146
+
142 147
   # Class Methods
143 148
   class << self
144 149
     def cannot_be_scheduled!

+ 23 - 0
app/models/agent_log.rb

@@ -0,0 +1,23 @@
1
+class AgentLog < ActiveRecord::Base
2
+  attr_accessible :agent, :inbound_event, :level, :message, :outbound_event
3
+
4
+  belongs_to :agent
5
+  belongs_to :inbound_event, :class_name => "Event"
6
+  belongs_to :outbound_event, :class_name => "Event"
7
+
8
+  validates_presence_of :agent, :message
9
+  validates_numericality_of :level, :only_integer => true, :greater_than_or_equal_to => 0, :less_than => 5
10
+
11
+  def self.log_for_agent(agent, message, options = {})
12
+    log = agent.logs.create! options.merge(:message => message)
13
+    if agent.logs.count > log_length
14
+      oldest_id_to_keep = agent.logs.limit(1).offset(log_length - 1).pluck("agent_logs.id")
15
+      agent.logs.where("agent_logs.id < ?", oldest_id_to_keep).delete_all
16
+    end
17
+    log
18
+  end
19
+
20
+  def self.log_length
21
+    ENV['AGENT_LOG_LENGTH'].present? ? ENV['AGENT_LOG_LENGTH'].to_i : 100
22
+  end
23
+end

+ 25 - 16
app/models/agents/website_agent.rb

@@ -66,29 +66,38 @@ module Agents
66 66
 
67 67
     def check
68 68
       hydra = Typhoeus::Hydra.new
69
+      log "Fetching #{options[:url]}"
69 70
       request = Typhoeus::Request.new(options[:url], :followlocation => true)
70
-      request.on_complete do |response|
71
+      request.on_failure do |response|
72
+        log "Failed: #{response.inspect}"
73
+      end
74
+      request.on_success do |response|
71 75
         doc = parse(response.body)
72 76
         output = {}
73 77
         options[:extract].each do |name, extraction_details|
74
-          if extraction_type == "json"
75
-            output[name] = Utils.values_at(doc, extraction_details[:path])
76
-          else
77
-            output[name] = doc.css(extraction_details[:css]).map { |node|
78
-              if extraction_details[:attr]
79
-                node.attr(extraction_details[:attr])
80
-              elsif extraction_details[:text]
81
-                node.text()
82
-              else
83
-                raise StandardError, ":attr or :text is required on HTML or XML extraction patterns"
84
-              end
85
-            }
86
-          end
78
+          result = if extraction_type == "json"
79
+                     output[name] = Utils.values_at(doc, extraction_details[:path])
80
+                   else
81
+                     output[name] = doc.css(extraction_details[:css]).map { |node|
82
+                       if extraction_details[:attr]
83
+                         node.attr(extraction_details[:attr])
84
+                       elsif extraction_details[:text]
85
+                         node.text()
86
+                       else
87
+                         log ":attr or :text is required on HTML or XML extraction patterns"
88
+                         return
89
+                       end
90
+                     }
91
+                   end
92
+          log "Extracting #{extraction_type} at #{extraction_details[:path] || extraction_details[:css]}: #{result}"
87 93
         end
88 94
 
89 95
         num_unique_lengths = options[:extract].keys.map { |name| output[name].length }.uniq
90 96
 
91
-        raise StandardError, "Got an uneven number of matches for #{options[:name]}: #{options[:extract].inspect}" unless num_unique_lengths.length == 1
97
+        if num_unique_lengths.length != 1
98
+          log "Got an uneven number of matches for #{options[:name]}: #{options[:extract].inspect}", :level => 4
99
+          return
100
+        end
92 101
 
93 102
         previous_payloads = events.order("id desc").limit(UNIQUENESS_LOOK_BACK).pluck(:payload).map(&:to_json) if options[:mode].to_s == "on_change"
94 103
         num_unique_lengths.first.times do |index|
@@ -101,7 +110,7 @@ module Agents
101 110
           end
102 111
 
103 112
           if !options[:mode] || options[:mode].to_s == "all" || (options[:mode].to_s == "on_change" && !previous_payloads.include?(result.to_json))
104
-            Rails.logger.info "Storing new result for '#{name}': #{result.inspect}"
113
+            log "Storing new result for '#{name}': #{result.inspect}"
105 114
             create_event :payload => result
106 115
           end
107 116
         end

+ 1 - 0
app/models/user.rb

@@ -23,6 +23,7 @@ class User < ActiveRecord::Base
23 23
 
24 24
   has_many :events, :order => "events.created_at desc", :dependent => :delete_all, :inverse_of => :user
25 25
   has_many :agents, :order => "agents.created_at desc", :dependent => :destroy, :inverse_of => :user
26
+  has_many :logs, :through => :agents, :class_name => "AgentLog"
26 27
 
27 28
   # Allow users to login via either email or username.
28 29
   def self.find_first_by_auth_conditions(warden_conditions)

+ 1 - 1
app/views/agents/index.html.erb

@@ -48,7 +48,7 @@
48 48
                   <%= link_to 'Edit', edit_agent_path(agent), class: "btn btn-mini" %>
49 49
                   <%= link_to 'Delete', agent_path(agent), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-mini" %>
50 50
                   <% if agent.can_be_scheduled? %>
51
-                      <%= link_to 'Run', run_agent_path(agent), method: :post, class: "btn btn-mini" %>
51
+                      <%= link_to 'Run', run_agent_path(agent, :return => "index"), method: :post, class: "btn btn-mini" %>
52 52
                   <% else %>
53 53
                       <%= link_to 'Run', "#", class: "btn btn-mini disabled" %>
54 54
                   <% end %>

+ 23 - 3
app/views/agents/show.html.erb

@@ -1,4 +1,4 @@
1
-<div class='container'>
1
+<div class='container agent-show'>
2 2
   <div class='row'>
3 3
     <div class='span12'>
4 4
 
@@ -11,6 +11,7 @@
11 11
             <li class='disabled'><a><i class='icon-picture'></i> Summary</a></li>
12 12
             <li class='active'><a href="#details" data-toggle="tab"><i class='icon-indent-left'></i> Details</a></li>
13 13
           <% end %>
14
+          <li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>"><i class='icon-list-alt'></i> Logs</a></li>
14 15
 
15 16
           <% if @agent.events.count > 0 %>
16 17
             <li><%= link_to '<i class="icon-random"></i> Events'.html_safe, events_path(:agent => @agent.to_param) %></li>
@@ -18,17 +19,24 @@
18 19
           <li><%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, agents_path %></li>
19 20
           <li><%= link_to '<i class="icon-pencil"></i> Edit'.html_safe, edit_agent_path(@agent) %></li>
20 21
 
21
-          <% if @agent.events.count > 0 %>
22
+          <% if @agent.can_be_scheduled? || @agent.events.count > 0 %>
22 23
             <li class="dropdown">
23 24
               <a class="dropdown-toggle" data-toggle="dropdown" href="#">Actions <b class="caret"></b></a>
24 25
               <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
26
+                <% if @agent.can_be_scheduled? %>
27
+                  <li>
28
+                    <%= link_to '<i class="icon-refresh"></i> Run'.html_safe, run_agent_path(@agent, :return => "show"), method: :post, :tabindex => "-1" %>
29
+                  </li>
30
+                <% end %>
31
+
32
+                <% if @agent.events.count > 0 %>
25 33
                   <li>
26 34
                     <%= link_to '<i class="icon-trash"></i> Delete all events'.html_safe, remove_events_agent_path(@agent), method: :delete, data: {confirm: 'Are you sure you want to delete ALL events for this Agent?'}, :tabindex => "-1" %>
27 35
                   </li>
36
+                <% end %>
28 37
               </ul>
29 38
             </li>
30 39
           <% end %>
31
-
32 40
         </ul>
33 41
 
34 42
         <div class="tab-content">
@@ -42,6 +50,18 @@
42 50
             <% end %>
43 51
           </div>
44 52
 
53
+          <div class="tab-pane" id="logs" data-agent-id="<%= @agent.id %>">
54
+            <h2>
55
+              <%= @agent.name %> Logs
56
+              <%= image_tag "spinner-arrows.gif", :class => "spinner" %>
57
+              <i class="icon-refresh action-icon refresh"></i>
58
+              <i class="icon-trash action-icon clear"></i>
59
+            </h2>
60
+            <div class='logs'>
61
+              Just a moment...
62
+            </div>
63
+          </div>
64
+
45 65
           <div class="tab-pane <%= agent_show_view(@agent).present? ? "" : "active" %>" id="details">
46 66
             <h2><%= @agent.name %> Details</h2>
47 67
 

+ 8 - 4
app/views/layouts/_messages.html.erb

@@ -1,6 +1,10 @@
1
-<% flash.each do |name, msg| %>
2
-  <div class="top-flash alert alert-<%= name == :notice ? "success" : "error" %>">
3
-    <a class="close" data-dismiss="alert">&#215;</a>
4
-    <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>
1
+<% if flash.keys.length > 0 %>
2
+  <div class="flash">
3
+    <% flash.each do |name, msg| %>
4
+      <div class="alert alert-<%= name == :notice ? "success" : "error" %>">
5
+        <a class="close" data-dismiss="alert">&#215;</a>
6
+        <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>
7
+      </div>
8
+    <% end %>
5 9
   </div>
6 10
 <% end %>

+ 5 - 0
app/views/layouts/_navigation.html.erb

@@ -14,6 +14,11 @@
14 14
         <span class="badge"><i class="icon-refresh icon-white"></i> <span class='number'>0</span></span>
15 15
       </a>
16 16
     </li>
17
+    <li id='event-indicator'>
18
+      <a href="#">
19
+        <span class="badge"><i class="icon-random icon-white"></i> <span class='number'>0</span> new events</span>
20
+      </a>
21
+    </li>
17 22
   <% end %>
18 23
 
19 24
   <li class="dropdown">

+ 30 - 0
app/views/logs/index.html.erb

@@ -0,0 +1,30 @@
1
+<table class='table table-striped logs'>
2
+  <tr>
3
+    <th>Message</th>
4
+    <th>When</th>
5
+    <th></th>
6
+  </tr>
7
+
8
+  <% @logs.each do |log| %>
9
+    <tr>
10
+      <td><%= truncate log.message, :length => 200, :omission => "..." %></td>
11
+      <td><%= time_ago_in_words log.created_at %> ago</td>
12
+
13
+      <td>
14
+        <div class="btn-group">
15
+          <% if log.inbound_event_id.present? %>
16
+            <%= link_to 'Event In', event_path(log.inbound_event), class: "btn btn-mini" %>
17
+          <% else %>
18
+            <%= link_to 'Event In', '#', class: "btn btn-mini disabled" %>
19
+          <% end %>
20
+
21
+          <% if log.outbound_event_id.present? %>
22
+            <%= link_to 'Event Out', event_path(log.outbound_event), class: "btn btn-mini" %>
23
+          <% else %>
24
+            <%= link_to 'Event Out', '#', class: "btn btn-mini disabled" %>
25
+          <% end %>
26
+        </div>
27
+      </td>
28
+    </tr>
29
+  <% end %>
30
+</table>

+ 6 - 0
config/routes.rb

@@ -11,6 +11,12 @@ Huginn::Application.routes.draw do
11 11
       get :event_descriptions
12 12
       get :diagram
13 13
     end
14
+
15
+    resources :logs, :only => [:index] do
16
+      collection do
17
+        delete :clear
18
+      end
19
+    end
14 20
   end
15 21
   resources :events, :only => [:index, :show, :destroy]
16 22
   match "/worker_status" => "worker_status#show"

+ 13 - 0
db/migrate/20130819160603_create_agent_logs.rb

@@ -0,0 +1,13 @@
1
+class CreateAgentLogs < ActiveRecord::Migration
2
+  def change
3
+    create_table :agent_logs do |t|
4
+      t.integer :agent_id, :null => false
5
+      t.text :message, :null => false
6
+      t.integer :level, :default => 3, :null => false
7
+      t.integer :inbound_event_id
8
+      t.integer :outbound_event_id
9
+
10
+      t.timestamps
11
+    end
12
+  end
13
+end

+ 11 - 1
db/schema.rb

@@ -11,7 +11,17 @@
11 11
 #
12 12
 # It's strongly recommended to check this file into your version control system.
13 13
 
14
-ActiveRecord::Schema.define(:version => 20130509053743) do
14
+ActiveRecord::Schema.define(:version => 20130819160603) do
15
+
16
+  create_table "agent_logs", :force => true do |t|
17
+    t.integer  "agent_id",                         :null => false
18
+    t.text     "message",                          :null => false
19
+    t.integer  "level",             :default => 3, :null => false
20
+    t.integer  "inbound_event_id"
21
+    t.integer  "outbound_event_id"
22
+    t.datetime "created_at",                       :null => false
23
+    t.datetime "updated_at",                       :null => false
24
+  end
15 25
 
16 26
   create_table "agents", :force => true do |t|
17 27
     t.integer  "user_id"

+ 12 - 1
spec/controllers/events_controller_spec.rb

@@ -12,10 +12,21 @@ describe EventsController do
12 12
       get :index
13 13
       assigns(:events).all? {|i| i.user.should == users(:bob) }.should be_true
14 14
     end
15
+
16
+    it "can filter by Agent" do
17
+      sign_in users(:bob)
18
+      get :index, :agent => agents(:bob_website_agent)
19
+      assigns(:events).length.should == agents(:bob_website_agent).events.length
20
+      assigns(:events).all? {|i| i.agent.should == agents(:bob_website_agent) }.should be_true
21
+
22
+      lambda {
23
+        get :index, :agent => agents(:jane_website_agent)
24
+      }.should raise_error(ActiveRecord::RecordNotFound)
25
+    end
15 26
   end
16 27
 
17 28
   describe "GET show" do
18
-    it "only shows Agents for the current user" do
29
+    it "only shows Events for the current user" do
19 30
       sign_in users(:bob)
20 31
       get :show, :id => events(:bob_website_agent_event).to_param
21 32
       assigns(:event).should eq(events(:bob_website_agent_event))

+ 37 - 0
spec/controllers/logs_controller_spec.rb

@@ -0,0 +1,37 @@
1
+require 'spec_helper'
2
+
3
+describe LogsController do
4
+  describe "GET index" do
5
+    it "can filter by Agent" do
6
+      sign_in users(:bob)
7
+      get :index, :agent_id => agents(:bob_weather_agent).id
8
+      assigns(:logs).length.should == agents(:bob_weather_agent).logs.length
9
+      assigns(:logs).all? {|i| i.agent.should == agents(:bob_weather_agent) }.should be_true
10
+    end
11
+
12
+    it "only loads Agents owned by the current user" do
13
+      sign_in users(:bob)
14
+      lambda {
15
+        get :index, :agent_id => agents(:jane_weather_agent).id
16
+      }.should raise_error(ActiveRecord::RecordNotFound)
17
+    end
18
+  end
19
+
20
+  describe "DELETE clear" do
21
+    it "deletes all logs for a specific Agent" do
22
+      sign_in users(:bob)
23
+      lambda {
24
+        delete :clear, :agent_id => agents(:bob_weather_agent).id
25
+      }.should change { AgentLog.count }.by(-1 * agents(:bob_weather_agent).logs.count)
26
+      assigns(:logs).length.should == 0
27
+      agents(:bob_weather_agent).logs.count.should == 0
28
+    end
29
+
30
+    it "only deletes logs for an Agent owned by the current user" do
31
+      sign_in users(:bob)
32
+      lambda {
33
+        delete :clear, :agent_id => agents(:jane_weather_agent).id
34
+      }.should raise_error(ActiveRecord::RecordNotFound)
35
+    end
36
+  end
37
+end

+ 15 - 0
spec/fixtures/agent_logs.yml

@@ -0,0 +1,15 @@
1
+log_for_jane_website_agent:
2
+  agent: jane_website_agent
3
+  message: "fetching some website data"
4
+
5
+log_for_bob_website_agent:
6
+  agent: bob_website_agent
7
+  message: "fetching some other website data"
8
+
9
+first_log_for_bob_weather_agent:
10
+  agent: bob_weather_agent
11
+  message: "checking the weather"
12
+
13
+second_log_for_bob_weather_agent:
14
+  agent: bob_weather_agent
15
+  message: "checking the weather again"

+ 15 - 0
spec/helpers/logs_helper_spec.rb

@@ -0,0 +1,15 @@
1
+require 'spec_helper'
2
+
3
+# Specs in this file have access to a helper object that includes
4
+# the AgentLogsHelper. For example:
5
+#
6
+# describe AgentLogsHelper do
7
+#   describe "string concat" do
8
+#     it "concats two strings with spaces" do
9
+#       expect(helper.concat_strings("this","that")).to eq("this that")
10
+#     end
11
+#   end
12
+# end
13
+describe LogsHelper do
14
+  pending "add some examples to (or delete) #{__FILE__}"
15
+end

+ 77 - 0
spec/models/agent_log_spec.rb

@@ -0,0 +1,77 @@
1
+require 'spec_helper'
2
+
3
+describe AgentLog do
4
+  describe "validations" do
5
+    before do
6
+      @log = AgentLog.new(:agent => agents(:jane_website_agent), :message => "The agent did something", :level => 3)
7
+      @log.should be_valid
8
+    end
9
+
10
+    it "requires an agent" do
11
+      @log.agent = nil
12
+      @log.should_not be_valid
13
+      @log.should have(1).error_on(:agent)
14
+    end
15
+
16
+    it "requires a message" do
17
+      @log.message = ""
18
+      @log.should_not be_valid
19
+      @log.message = nil
20
+      @log.should_not be_valid
21
+      @log.should have(1).error_on(:message)
22
+    end
23
+
24
+    it "requires a valid log level" do
25
+      @log.level = nil
26
+      @log.should_not be_valid
27
+      @log.should have(1).error_on(:level)
28
+
29
+      @log.level = -1
30
+      @log.should_not be_valid
31
+      @log.should have(1).error_on(:level)
32
+
33
+      @log.level = 5
34
+      @log.should_not be_valid
35
+      @log.should have(1).error_on(:level)
36
+
37
+      @log.level = 4
38
+      @log.should be_valid
39
+
40
+      @log.level = 0
41
+      @log.should be_valid
42
+    end
43
+  end
44
+
45
+  describe "#log_for_agent" do
46
+    it "creates AgentLogs" do
47
+      log = AgentLog.log_for_agent(agents(:jane_website_agent), "some message", :level => 4, :outbound_event => events(:jane_website_agent_event))
48
+      log.should_not be_new_record
49
+      log.agent.should == agents(:jane_website_agent)
50
+      log.outbound_event.should == events(:jane_website_agent_event)
51
+      log.message.should == "some message"
52
+      log.level.should == 4
53
+    end
54
+
55
+    it "cleans up old logs when there are more than log_length" do
56
+      stub(AgentLog).log_length { 4 }
57
+      AgentLog.log_for_agent(agents(:jane_website_agent), "message 1")
58
+      AgentLog.log_for_agent(agents(:jane_website_agent), "message 2")
59
+      AgentLog.log_for_agent(agents(:jane_website_agent), "message 3")
60
+      AgentLog.log_for_agent(agents(:jane_website_agent), "message 4")
61
+      agents(:jane_website_agent).logs.order("agent_logs.id desc").first.message.should == "message 4"
62
+      agents(:jane_website_agent).logs.order("agent_logs.id desc").last.message.should == "message 1"
63
+      AgentLog.log_for_agent(agents(:jane_website_agent), "message 5")
64
+      agents(:jane_website_agent).logs.order("agent_logs.id desc").first.message.should == "message 5"
65
+      agents(:jane_website_agent).logs.order("agent_logs.id desc").last.message.should == "message 2"
66
+      AgentLog.log_for_agent(agents(:jane_website_agent), "message 6")
67
+      agents(:jane_website_agent).logs.order("agent_logs.id desc").first.message.should == "message 6"
68
+      agents(:jane_website_agent).logs.order("agent_logs.id desc").last.message.should == "message 3"
69
+    end
70
+  end
71
+
72
+  describe "#log_length" do
73
+    it "defaults to 100" do
74
+      AgentLog.log_length.should == 100
75
+    end
76
+  end
77
+end

+ 4 - 5
spec/models/agents/website_agent_spec.rb

@@ -35,11 +35,10 @@ describe Agents::WebsiteAgent do
35 35
     end
36 36
 
37 37
     it "should log an error if the number of results for a set of extraction patterns differs" do
38
-      lambda {
39
-        @site[:extract][:url][:css] = "div"
40
-        @checker.options = @site
41
-        @checker.check
42
-      }.should raise_error(StandardError, /Got an uneven number of matches/)
38
+      @site[:extract][:url][:css] = "div"
39
+      @checker.options = @site
40
+      @checker.check
41
+      @checker.logs.first.message.should =~ /Got an uneven number of matches/
43 42
     end
44 43
   end
45 44
 

+ 0 - 0
vendor/assets/javascripts/.gitkeep


+ 0 - 0
vendor/assets/stylesheets/.gitkeep